/**
 * Copyright 2016 Smart Society Services B.V.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */
package com.alliander.osgp.domain.core.valueobjects.smartmetering;

import java.io.Serializable;

import org.joda.time.LocalDate;

public class CosemDate implements Serializable, Comparable<CosemDate> {

    private static final long serialVersionUID = -3965207413972253042L;

    public static final int YEAR_NOT_SPECIFIED = 0xFFFF;

    public static final int MONTH_DAYLIGHT_SAVINGS_END = 0xFD;
    public static final int MONTH_DAYLIGHT_SAVINGS_BEGIN = 0xFE;
    public static final int MONTH_NOT_SPECIFIED = 0xFF;

    public static final int DAY_OF_MONTH_2ND_LAST_DAY = 0xFD;
    public static final int DAY_OF_MONTH_LAST_DAY = 0xFE;
    public static final int DAY_OF_MONTH_NOT_SPECIFIED = 0xFF;
    public static final int DAY_OF_MONTH_RESERVED_MIN = 0xE0;
    public static final int DAY_OF_MONTH_RESERVED_MAX = 0xFC;

    public static final int DAY_OF_WEEK_NOT_SPECIFIED = 0xFF;

    private final int year;
    private final int month;
    private final int dayOfMonth;
    private final int dayOfWeek;

    public CosemDate(final int year, final int month, final int dayOfMonth, final int dayOfWeek) {
        this.checkInputs(year, month, dayOfMonth, dayOfWeek);
        this.year = year;
        this.month = month;
        this.dayOfMonth = dayOfMonth;
        this.dayOfWeek = dayOfWeek;
    }

    public CosemDate(final int year, final int month, final int dayOfMonth) {
        this(year, month, dayOfMonth, DAY_OF_WEEK_NOT_SPECIFIED);
    }

    public CosemDate(final LocalDate date) {
        this(date.getYear(), date.getMonthOfYear(), date.getDayOfMonth(), DAY_OF_WEEK_NOT_SPECIFIED);
    }

    public CosemDate(final CosemDate cosemDate) {
        this(cosemDate.getYear(), cosemDate.getMonth(), cosemDate.getDayOfMonth(), cosemDate.getDayOfWeek());
    }

    public CosemDate() {
        this(LocalDate.now());
    }

    private void checkInputs(final int year, final int month, final int dayOfMonth, final int dayOfWeek) {
        this.checkYear(year);
        this.checkMonth(month);
        this.checkDayOfMonth(dayOfMonth);
        this.checkDayOfWeek(dayOfWeek);
    }

    private void checkYear(final int year) {
        if (year < 0 || year > 0xFFFF) {
            throw new IllegalArgumentException("Year not in [0..65535]: " + year);
        }
    }

    private void checkMonth(final int month) {
        if (!this.isValidMonth(month)) {
            throw new IllegalArgumentException("Month not in [1..12, 0xFD, 0xFE, 0xFF]: " + month);
        }
    }

    private boolean isValidMonth(final int month) {
        return this.isSpecificMonth(month) || this.isSpecialMonth(month);
    }

    private boolean isSpecificMonth(final int month) {
        return month >= 1 && month <= 12;
    }

    private boolean isSpecialMonth(final int month) {
        return MONTH_DAYLIGHT_SAVINGS_END == month || MONTH_DAYLIGHT_SAVINGS_BEGIN == month
                || MONTH_NOT_SPECIFIED == month;
    }

    private void checkDayOfMonth(final int dayOfMonth) {
        if (!this.isValidDayOfMonth(dayOfMonth)) {
            throw new IllegalArgumentException("DayOfMonth not in [1..31, 0xE0..0xFF]: " + dayOfMonth);
        }
    }

    private boolean isValidDayOfMonth(final int dayOfMonth) {
        return this.isSpecificDayOfMonth(dayOfMonth) || this.isSpecialDayOfMonth(dayOfMonth);
    }

    private boolean isSpecificDayOfMonth(final int dayOfMonth) {
        return dayOfMonth >= 1 && dayOfMonth <= 31;
    }

    private boolean isSpecialDayOfMonth(final int dayOfMonth) {
        return DAY_OF_MONTH_2ND_LAST_DAY == dayOfMonth || DAY_OF_MONTH_LAST_DAY == dayOfMonth
                || DAY_OF_MONTH_NOT_SPECIFIED == dayOfMonth || this.isReservedDayOfMonth(dayOfMonth);
    }

    private boolean isReservedDayOfMonth(final int dayOfMonth) {
        return dayOfMonth >= DAY_OF_MONTH_RESERVED_MIN && dayOfMonth <= DAY_OF_MONTH_RESERVED_MAX;
    }

    private void checkDayOfWeek(final int dayOfWeek) {
        if (!this.isValidDayOfWeek(dayOfWeek)) {
            throw new IllegalArgumentException("DayOfWeek not in [1..7, 0xFF]: " + dayOfWeek);
        }
    }

    private boolean isValidDayOfWeek(final int dayOfWeek) {
        return this.isSpecificDayOfWeek(dayOfWeek) || DAY_OF_WEEK_NOT_SPECIFIED == dayOfWeek;
    }

    private boolean isSpecificDayOfWeek(final int dayOfWeek) {
        return dayOfWeek >= 1 && dayOfWeek <= 7;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        if (!this.isDayOfWeekNotSpecified()) {
            this.appendDayOfWeek(sb);
            sb.append(' ');
        }
        this.appendYear(sb);
        this.appendMonth(sb);
        this.appendDayOfMonth(sb);
        this.appendDayOfWeek(sb);
        return sb.toString();
    }

    private void appendYear(final StringBuilder sb) {
        if (this.isYearNotSpecified()) {
            sb.append(String.format("%04X", this.year));
        } else {
            sb.append(String.format("%04d", this.year));
        }
    }

    private void appendMonth(final StringBuilder sb) {
        if (this.isSpecialMonth(this.month)) {
            sb.append(String.format("%02X", this.month));
        } else {
            sb.append(String.format("%02d", this.month));
        }
    }

    private void appendDayOfMonth(final StringBuilder sb) {
        if (this.isSpecialDayOfMonth(this.dayOfMonth)) {
            sb.append(String.format("%02X", this.dayOfMonth));
        } else {
            sb.append(String.format("%02d", this.dayOfMonth));
        }
    }

    private void appendDayOfWeek(final StringBuilder sb) {
        switch (this.dayOfWeek) {
        case 1:
            sb.append("Monday");
            break;
        case 2:
            sb.append("Tuesday");
            break;
        case 3:
            sb.append("Wednesday");
            break;
        case 4:
            sb.append("Thursday");
            break;
        case 5:
            sb.append("Friday");
            break;
        case 6:
            sb.append("Saturday");
            break;
        case 7:
            sb.append("Sunday");
            break;
        case 0xFF:
            sb.append("Day of week not specified");
            break;
        default:
            throw new AssertionError("DayOfWeek value unknown: " + this.dayOfWeek);
        }
    }

    /**
     * @return the year for this {@link CosemDate}.
     *
     * @see #YEAR_NOT_SPECIFIED
     */
    public int getYear() {
        return this.year;
    }

    /**
     * @return the month for this {@link CosemDate} (January=1 .. December=12).
     *
     * @see #MONTH_DAYLIGHT_SAVINGS_END
     * @see #MONTH_DAYLIGHT_SAVINGS_BEGIN
     * @see #MONTH_NOT_SPECIFIED
     */
    public int getMonth() {
        return this.month;
    }

    /**
     * @return the day of month for this {@link CosemDate} (1..31).
     *
     * @see #DAY_OF_MONTH_2ND_LAST_DAY
     * @see #DAY_OF_MONTH_LAST_DAY
     * @see #DAY_OF_MONTH_NOT_SPECIFIED
     * @see #DAY_OF_MONTH_RESERVED_MIN
     * @see #DAY_OF_MONTH_RESERVED_MAX
     */
    public int getDayOfMonth() {
        return this.dayOfMonth;
    }

    /**
     * @return the day of the week for this {@link CosemDate} (Monday=1 ..
     *         Sunday=7).
     *
     * @see #DAY_OF_WEEK_NOT_SPECIFIED
     */
    public int getDayOfWeek() {
        return this.dayOfWeek;
    }

    /**
     * @return {@code true} if the values for {@code year}, {@code month} and
     *         {@code dayOfMonth} contain values for regular dates;
     *         {@code false} if wildcards are used in the fields mentioned.
     */
    public boolean isLocalDateSpecified() {
        return YEAR_NOT_SPECIFIED != this.year && this.isSpecificMonth(this.month)
                && this.isSpecificDayOfMonth(this.dayOfMonth);
    }

    /**
     * Returns this {@link CosemDate} as {@link LocalDate} if {@code year},
     * {@code month} and {@code dayOfMonth} do not contain wildcard values.
     *
     * @return this {@link CosemDate} as {@link LocalDate}, or {@code null} if
     *         not {@link #isLocalDateSpecified()}.
     *
     * @see #isLocalDateSpecified()
     */
    public LocalDate asLocalDate() {
        if (this.isLocalDateSpecified()) {
            return new LocalDate(this.year, this.month, this.dayOfMonth);
        }
        return null;
    }

    public boolean isYearNotSpecified() {
        return YEAR_NOT_SPECIFIED == this.year;
    }

    public boolean isMonthForDaylightSavingsEnd() {
        return MONTH_DAYLIGHT_SAVINGS_END == this.month;
    }

    public boolean isMonthForDaylightSavingsBegin() {
        return MONTH_DAYLIGHT_SAVINGS_BEGIN == this.month;
    }

    public boolean isMonthNotSpecified() {
        return MONTH_NOT_SPECIFIED == this.month;
    }

    public boolean isDayOfMonthNotSpecified() {
        return DAY_OF_MONTH_NOT_SPECIFIED == this.dayOfMonth;
    }

    public boolean isSecondLastDayOfMonth() {
        return DAY_OF_MONTH_2ND_LAST_DAY == this.dayOfMonth;
    }

    public boolean isLastDayOfMonth() {
        return DAY_OF_MONTH_LAST_DAY == this.dayOfMonth;
    }

    public boolean isReservedDayOfMonth() {
        return this.isReservedDayOfMonth(this.dayOfMonth);
    }

    public boolean isDayOfWeekNotSpecified() {
        return DAY_OF_WEEK_NOT_SPECIFIED == this.dayOfWeek;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + this.dayOfMonth;
        result = prime * result + this.dayOfWeek;
        result = prime * result + this.month;
        result = prime * result + this.year;
        return result;
    }

    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        final CosemDate other = (CosemDate) obj;

        return this.dayOfMonth == other.dayOfMonth && this.dayOfWeek == other.dayOfWeek && this.month == other.month
                && this.year == other.year;
    }

    @Override
    public int compareTo(final CosemDate o) {
        if (this.compareNotEqual(this.year, o.year, YEAR_NOT_SPECIFIED)) {
            return this.year - o.year;
        }
        if (this.compareNotEqual(this.month, o.month, MONTH_NOT_SPECIFIED)) {
            return this.month - o.month;
        }
        if (this.compareNotEqual(this.dayOfMonth, o.dayOfMonth, DAY_OF_MONTH_NOT_SPECIFIED)) {
            return this.dayOfMonth - o.dayOfMonth;
        }
        if (this.compareNotEqual(this.dayOfWeek, o.dayOfWeek, DAY_OF_WEEK_NOT_SPECIFIED)) {
            return this.dayOfWeek - o.dayOfWeek;
        }

        return 0;
    }

    private boolean compareNotEqual(final int value, final int compareValue, final int unspecifiedConstant) {
        return value != unspecifiedConstant && compareValue != unspecifiedConstant && value - compareValue != 0;
    }
}
